package utils

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/go-openapi/jsonpointer"
)

// TokenExtractor extracts tokens from API responses
type TokenExtractor struct {
	jsonPathCache map[string]jsonpointer.Pointer // cache compiled JSON pointers
	mutex         sync.RWMutex
}

// NewTokenExtractor creates a new TokenExtractor instance
func NewTokenExtractor() *TokenExtractor {
	return &TokenExtractor{
		jsonPathCache: make(map[string]jsonpointer.Pointer),
	}
}

// ExtractToken extracts token from response body based on the provided configuration
func (te *TokenExtractor) ExtractToken(responseBody []byte, config *AuthConfig) (*AuthState, error) {
	if config == nil {
		return nil, NewAuthError(ErrTypeConfigInvalid, "auth config cannot be nil", "")
	}

	if len(responseBody) == 0 {
		return nil, NewAuthError(ErrTypeTokenExtraction, "response body is empty", "")
	}

	// Parse response as JSON
	var responseData interface{}
	if err := json.Unmarshal(responseBody, &responseData); err != nil {
		return nil, NewAuthError(ErrTypeTokenExtraction, 
			"failed to parse response as JSON", err.Error())
	}

	// Extract token using JSON path
	token, err := te.ExtractFromJSONPath(responseBody, config.TokenPath)
	if err != nil {
		return nil, err
	}

	// Validate token format
	if err := te.ValidateTokenFormat(token, config.TokenFormat); err != nil {
		return nil, err
	}

	// Create AuthState
	authState := &AuthState{
		Token:     token,
		TokenType: config.TokenFormat,
		CreatedAt: time.Now(),
		Metadata:  make(map[string]string),
	}

	// Extract expiration time if configured
	if config.ExpiresPath != "" {
		expiresAt, err := te.ParseExpiration(responseBody, config.ExpiresPath)
		if err != nil {
			// Log warning but don't fail - expiration is optional
			// In a real implementation, you'd use proper logging here
			fmt.Printf("Warning: failed to parse expiration time: %v\n", err)
		} else {
			authState.ExpiresAt = expiresAt
		}
	}

	// Add metadata from config
	if config.Metadata != nil {
		for k, v := range config.Metadata {
			authState.Metadata[k] = v
		}
	}

	// Add additional metadata
	authState.Metadata["token_format"] = config.TokenFormat
	if config.HeaderName != "" {
		authState.Metadata["header_name"] = config.HeaderName
	}
	if config.QueryParam != "" {
		authState.Metadata["query_param"] = config.QueryParam
	}
	if config.TokenPrefix != "" {
		authState.Metadata["token_prefix"] = config.TokenPrefix
	}

	return authState, nil
}

// ExtractFromJSONPath extracts a value from JSON data using JSON pointer syntax
func (te *TokenExtractor) ExtractFromJSONPath(data []byte, path string) (string, error) {
	if path == "" {
		return "", NewAuthError(ErrTypeConfigInvalid, "token path cannot be empty", "")
	}

	// Get or create cached JSON pointer
	pointer, err := te.getOrCreatePointer(path)
	if err != nil {
		return "", NewAuthError(ErrTypeConfigInvalid, 
			fmt.Sprintf("invalid JSON path: %s", path), err.Error())
	}

	// Parse JSON data
	var jsonData interface{}
	if err := json.Unmarshal(data, &jsonData); err != nil {
		return "", NewAuthError(ErrTypeTokenExtraction, 
			"failed to parse JSON data", err.Error())
	}

	// Extract value using JSON pointer
	value, _, err := pointer.Get(jsonData)
	if err != nil {
		return "", NewAuthError(ErrTypeTokenExtraction, 
			fmt.Sprintf("failed to extract value at path '%s'", path), err.Error())
	}

	// Convert value to string
	switch v := value.(type) {
	case string:
		return v, nil
	case float64:
		return strconv.FormatFloat(v, 'f', -1, 64), nil
	case int:
		return strconv.Itoa(v), nil
	case bool:
		return strconv.FormatBool(v), nil
	default:
		// Try to marshal as JSON for complex types
		jsonBytes, err := json.Marshal(v)
		if err != nil {
			return "", NewAuthError(ErrTypeTokenExtraction, 
				fmt.Sprintf("cannot convert value at path '%s' to string", path), 
				fmt.Sprintf("value type: %T", v))
		}
		return string(jsonBytes), nil
	}
}

// ParseExpiration extracts and parses expiration time from JSON data
func (te *TokenExtractor) ParseExpiration(data []byte, path string) (*time.Time, error) {
	if path == "" {
		return nil, NewAuthError(ErrTypeConfigInvalid, "expiration path cannot be empty", "")
	}

	// Extract expiration value
	expirationStr, err := te.ExtractFromJSONPath(data, path)
	if err != nil {
		return nil, err
	}

	// Try to parse as different time formats
	var expiresAt time.Time
	var parseErr error

	// Try parsing as integer first (Unix timestamp or expires_in)
	if timestamp, err := strconv.ParseInt(expirationStr, 10, 64); err == nil {
		// Check if it's a Unix timestamp in milliseconds (> year 2001)
		if timestamp > 1e12 {
			expiresAt = time.Unix(timestamp/1000, (timestamp%1000)*1e6)
			return &expiresAt, nil
		}
		// Check if it's a Unix timestamp in seconds (> year 2001)
		if timestamp > 1e9 {
			expiresAt = time.Unix(timestamp, 0)
			return &expiresAt, nil
		}
		// Otherwise treat as expires_in seconds
		expiresAt = time.Now().Add(time.Duration(timestamp) * time.Second)
		return &expiresAt, nil
	}

	// Try parsing as RFC3339 format
	if expiresAt, parseErr = time.Parse(time.RFC3339, expirationStr); parseErr == nil {
		return &expiresAt, nil
	}

	// Try parsing as RFC3339Nano format
	if expiresAt, parseErr = time.Parse(time.RFC3339Nano, expirationStr); parseErr == nil {
		return &expiresAt, nil
	}

	// Try parsing as ISO 8601 format
	if expiresAt, parseErr = time.Parse("2006-01-02T15:04:05Z", expirationStr); parseErr == nil {
		return &expiresAt, nil
	}

	// Try parsing as common date format
	if expiresAt, parseErr = time.Parse("2006-01-02 15:04:05", expirationStr); parseErr == nil {
		return &expiresAt, nil
	}

	return nil, NewAuthError(ErrTypeTokenExtraction, 
		fmt.Sprintf("failed to parse expiration time: %s", expirationStr),
		"supported formats: Unix timestamp, RFC3339, ISO 8601, or expires_in seconds")
}

// ValidateTokenFormat validates that the extracted token matches the expected format
func (te *TokenExtractor) ValidateTokenFormat(token string, format string) error {
	if token == "" {
		return NewAuthError(ErrTypeTokenInvalid, "extracted token is empty", "")
	}

	switch format {
	case "bearer":
		// Bearer tokens should be non-empty strings
		// Additional validation could be added here (e.g., JWT format validation)
		return nil
	case "api_key":
		// API keys should be non-empty strings
		// Additional validation could be added here (e.g., length, character set)
		return nil
	case "custom":
		// Custom tokens should be non-empty strings
		// Validation depends on the specific API requirements
		return nil
	default:
		return NewAuthError(ErrTypeTokenInvalid, 
			fmt.Sprintf("unsupported token format: %s", format),
			"supported formats: bearer, api_key, custom")
	}
}

// getOrCreatePointer gets a cached JSON pointer or creates and caches a new one
func (te *TokenExtractor) getOrCreatePointer(path string) (jsonpointer.Pointer, error) {
	te.mutex.RLock()
	if pointer, exists := te.jsonPathCache[path]; exists {
		te.mutex.RUnlock()
		return pointer, nil
	}
	te.mutex.RUnlock()

	// Convert path to JSON pointer format if needed
	jsonPointerPath := te.convertToJSONPointer(path)

	// Create new pointer
	pointer, err := jsonpointer.New(jsonPointerPath)
	if err != nil {
		return jsonpointer.Pointer{}, err
	}

	// Cache the pointer
	te.mutex.Lock()
	te.jsonPathCache[path] = pointer
	te.mutex.Unlock()

	return pointer, nil
}

// convertToJSONPointer converts various path formats to JSON pointer format
func (te *TokenExtractor) convertToJSONPointer(path string) string {
	// If path already starts with "/", assume it's already in JSON pointer format
	if strings.HasPrefix(path, "/") {
		return path
	}

	// Convert dot notation to JSON pointer format
	// e.g., "data.token" -> "/data/token"
	parts := strings.Split(path, ".")
	return "/" + strings.Join(parts, "/")
}

// ClearCache clears the JSON pointer cache
func (te *TokenExtractor) ClearCache() {
	te.mutex.Lock()
	defer te.mutex.Unlock()
	te.jsonPathCache = make(map[string]jsonpointer.Pointer)
}

// GetCacheSize returns the current size of the JSON pointer cache
func (te *TokenExtractor) GetCacheSize() int {
	te.mutex.RLock()
	defer te.mutex.RUnlock()
	return len(te.jsonPathCache)
}